package cn.ckx.coding.common.aspect;

import cn.ckx.coding.common.annotation.http.ApiIdempotent;
import cn.ckx.coding.common.exception.BusinessError;
import cn.hutool.http.HttpStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @ClassName ApiIdempotentAspect
 * @Description 接口幂等切面，基于token,基于redis
 * 
 * 为需要保证幂等性的每一次请求创建一个唯一标识token
 * 先获取token,并将此token存入redis,请求接口时,将此token放到header或者作为请求参数请求接口,后端接口判断redis中是否存在此token:
 * 如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示
 * 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可。
 * 
 * @Author kunxin.cai
 * @Date 2021/3/3
 * @Version 1.0
 **/
@Slf4j
@Component
@Aspect
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@ConditionalOnClass(StringRedisTemplate.class)
public class ApiIdempotentAspect {

    private final StringRedisTemplate redisTemplate;

    private static final String REDIS_KEY_FORMAT = "api-idempotent:%s:%s:string";

    @Pointcut("@annotation(apiIdempotent)")
    public void pointcut(ApiIdempotent apiIdempotent) {

    }

    @Around(value = "pointcut(apiIdempotent)",argNames = "proceedingJoinPoint,apiIdempotent")
    public Object idempotentVerify(ProceedingJoinPoint proceedingJoinPoint, ApiIdempotent apiIdempotent) throws Throwable {
        MethodSignature methodSignature =(MethodSignature) proceedingJoinPoint.getSignature();
        String[] argNames = apiIdempotent.argNames();
        if (argNames.length > 0){
            HttpServletRequest httpServletRequest = getHttpServletRequest();
            String key = Arrays.stream(argNames).map(item -> {
                Object parameter = httpServletRequest.getParameter(item);
                return parameter == null ? "":String.valueOf(parameter);
            }).filter(StringUtils::isNotBlank).collect(Collectors.joining("_"));
            if (StringUtils.isNotBlank(key)){
                String redisKey = String.format(REDIS_KEY_FORMAT, methodSignature.getMethod().getName(), key);
                Optional<Boolean> success;
                if (apiIdempotent.onceSubmitted()) {
                    success = Optional.ofNullable(redisTemplate.opsForValue().setIfAbsent(redisKey, redisKey));
                }else {
                    success = Optional.ofNullable(redisTemplate.opsForValue().setIfAbsent(redisKey, redisKey, apiIdempotent.timeliness(), TimeUnit.SECONDS));
                }
                if (success.orElse(true)) {
                    return proceedingJoinPoint.proceed();
                }
                log.warn("Don't submit the form twice , method is [{}], the form parameter is {}", methodSignature.getMethod().getName(), proceedingJoinPoint.getArgs());
                throw BusinessError.builder(HttpStatus.HTTP_BAD_REQUEST, "请勿重复提交").build();
            }
        }
        return proceedingJoinPoint.proceed();
    }

    private HttpServletRequest getHttpServletRequest() {
        return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
    }

}
